const MD5 = require("md5");
const base64 = require("js-base64");
base64.Base64.extendString();
const axios = require("axios").default.create({
  headers: {
    "User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36 OPR/51.0.2830.55"
  }
});
const qs = require("querystring");
const rannum = require("random-number");
const util = require("util");
const path = require("path");
const fs = require("fs");
const url = require("url");
const FormData = require("form-data");
const buffer = require("buffer");
const http = require("http");
const filesize = require("filesize");
const get_token = require("./get-token");
const Toolbox = require("../Toolbox");


class Pianke7Niu {
  constructor() {
    /**@type {(full_path:string,parts_count:number,part_index:number)=>string} */
    this.default_part_name_func = (full_path, parts_count, part_index) => {
      parts_count = parseInt(parts_count);
      if (parts_count == 1) {
        return path.basename(full_path);
      } else {
        let max_length = String(parts_count).length;
        let str_index = String(part_index);
        while (str_index.length < max_length) {
          str_index = `0${str_index}`
        }
        return `${path.basename(full_path)}.sf-part${str_index}`
      }
    }
  }

  /**
   * 
   * @param {import("fs").ReadStream|String} stream 
   * @param {Strings} define_filename 
   * @returns {Promise<{ok:Boolean,msg:String,url:String}>}
   */
  uploadStream(stream, define_filename = "test.bin") {
    const upload_url = "http://upload.qiniu.com/";
    const parsed = url.parse(upload_url);
    return new Promise(async resolve => {
      let ext = path.extname(define_filename);
      ext = ext.replace(".", "")
      let o_getToken = await get_token.getUploadToken(ext);
      if (!o_getToken.ok) {
        return resolve({
          ok: false,
          msg: `error when getUploadToken():${o_getToken.msg}`,
          url: ''
        })
      }
      let token = o_getToken.token;
      let remote_filename = o_getToken.remote_filename;
      let form = new FormData;
      form.append("file", stream, {
        filename: `${Math.random()}.${ext}`
      });
      form.append("key", remote_filename);
      form.append("token", token);
      let headers = {
        "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36",
        "Referer": "http://pianke.me/editor/",
        'X-Requested-With': 'XMLHttpRequest',
      };
      let request = http.request({
        method: "POST",
        host: parsed.host,
        path: parsed.path,
        headers: {
          ...form.getHeaders(),
          ...headers
        }
      });
      form.on("error", err => {
        resolve({
          ok: false,
          msg: `form.on(error):${err.message}`,
          url: ""
        })
      });
      form.pipe(request);
      let response_flag = false;
      form.on("end", () => {
        setTimeout(() => {
          if (!response_flag) {
            resolve({
              ok: false,
              msg: `[LATE RESPONSE] NO HTTP RESPONSE after form.end 120 seconds`
            })
          }
        }, 60 * 1000 * 2);
      });
      request.on("response", (resp) => {
        response_flag = true;
        let buf = new buffer.Buffer("");
        resp.on("data", chunk => {
          buf += chunk;
        })
        resp.on("end", $ => {
          try {
            let obj = JSON.parse(buf);
            if (!obj['key']) {
              return resolve({
                ok: false,
                msg: `!obj['key'],obj is ${util.inspect(buf)}`,
                url: ""
              })
            }
            let file_url = `https://qnimg.pianke.me/${obj['key']}`;
            resolve({
              ok: true,
              msg: "ok",
              url: file_url
            })
          } catch (e) {
            resolve({
              ok: false,
              url: "",
              msg: `error when try extract url :${e.message ? e.message : util.inspect(e)}`
            })
          }
        })
      });
      request.on("error", err => {
        resolve({
          ok: false,
          msg: `request.on(error):${err.message}`,
          url: ""
        })
      });
    })
  }


  /**
   *  @returns {Promise<{ok:Boolean,url:String,msg:String,file_size:String,speed:String}>}
   * @param {String} full_path 
   */
  getUploadSpeed(full_path) {
    return new Promise(async resolve => {
      let o_stats = await Toolbox.getStats(full_path);
      if ((!o_stats.ok) || (!o_stats.stats.isFile())) {
        return resolve({
          ok: false,
          msg: o_stats.msg ? o_stats.msg : `${full_path} is NOT File`
        })
      }
      let start_time = Date.now();
      console.log("start uploading...", path.basename(full_path));
      let o_up = await this.uploadStream(fs.createReadStream(full_path));
      let end_time = Date.now();
      if (!o_up) {
        return resolve({
          ok: false,
          msg: o_up.msg,
        })
      } else {
        let bytes_per_second = o_stats.stats.size / ((end_time - start_time) / 1000)
        resolve({
          ok: true,
          msg: "ok",
          url: o_up.url,
          file_size: filesize(o_stats.stats.size),
          speed: filesize(bytes_per_second) + "/s"
        })
      }

    })
  }

  /**
   *
   * @returns {Promise<{ok:Boolean,msg:String,files:Array<{name:String,url:String}>}>}
   * @param {String} full_file_path
   * @param {(full_path:string,parts_count:number,part_index:number)=>string} part_name_func 
   * @param {number} [size_limit=1000*1024*1024]
   * @memberof Pianke7Niu
   */
  uploadFile(full_file_path, size_limit = 1000 * 1024 * 1024, part_name_func = this.default_part_name_func) {
    return new Promise(async resolve => {
      let o_stats = await Toolbox.getStats(full_file_path);
      if (!o_stats.ok) {
        return resolve({
          ok: false,
          msg: `fail to get stats of ${file_full_path} : ${o_stats.msg}`
        })
      }
      if (!o_stats.stats.isFile()) {
        return resolve({
          ok: false,
          msg: ` ${file_full_path} : it is not a file`
        })
      }
      let CHUNK_MAX_SIZE = size_limit;
      let CHUNKS_COUNT = Math.ceil(o_stats.stats.size / CHUNK_MAX_SIZE);
      /**@type {Array<{name:String,url:String}>} */
      let files = [];
      for (let i = 0; i < CHUNKS_COUNT; i++) {
        let start = i * CHUNK_MAX_SIZE;
        let end = start + CHUNK_MAX_SIZE - 1;
        if (end >= o_stats.stats.size) {
          end = o_stats.stats.size - 1;
        }
        let o_upChunk = await this.uploadStream(
          fs.createReadStream(full_file_path, {
            start: start,
            end: end
          }),
          part_name_func(full_file_path, CHUNKS_COUNT, i)
        )
        if (!o_upChunk.ok) {
          return resolve({
            ok: false,
            msg: `error when upload part ${i + 1}:${o_upChunk.msg}`
          })
        }
        files.push({
          name: part_name_func(full_file_path, CHUNKS_COUNT, i),
          url: o_upChunk.url
        })
      }
      resolve({
        ok: true,
        msg: "ok",
        files: files
      })
    })
  }

  /**
   *
   * @returns {Promise<{ok:Boolean,errors:string[],toHTML:()=>string,files:Array<{path:string,chunks:Array<{url:string,name:string}>  }>}>}
   * @param {String} full_path_of_dir
   * @param {number} [size_limit=1000 * 1024 * 1024]
   * @param {String} base_dir
   * @param {(full_path:string,parts_count:number,part_index:number)=>string} [part_name_func=this.default_part_name_func]
   * @memberof Pianke7Niu
   */
  safelyUploadDir(full_path_of_dir, size_limit = 1000 * 1024 * 1024, base_dir = full_path_of_dir, part_name_func = this.default_part_name_func) {
    return new Promise(async resolve => {
      let o_stats = await Toolbox.getStats(full_path_of_dir);
      if (!o_stats.ok) {
        let errmsg = `fail to get stats of ${full_path_of_dir} : ${o_stats.msg}`
        return resolve({
          ok: false,
          errors: [errmsg],
          toHTML: () => {
            return `<font color="red">${errmsg}</font>`
          }
        })
      }
      if (!o_stats.stats.isDirectory()) {
        let msg = ` ${full_path_of_dir} : it is not a folder`
        return resolve({
          ok: false,
          errors: [msg],
          toHTML: () => `<font color="red">${msg}</font>`
        })
      }
      let safeList = await Toolbox.safeListDir(full_path_of_dir);
      /**@type {string[]} */
      let errors = [];
      /**@type {Array<{path:string,chunks:Array<{url:string,name:string}> } >} */
      let files = [];
      for (let item of safeList) {
        if (item.stats.isDirectory()) {
          let o_uploadChildFolder = await this.safelyUploadDir(item.full_path, size_limit, base_dir, part_name_func);
          errors = errors.concat(o_uploadChildFolder.errors);
          files = files.concat(o_uploadChildFolder.files);
        } else if (item.stats.isFile()) {
          for (let r = 1; r <= 5; r++) {
            //re try 机制
            if (r == 5) {
              break;
            }
            let o_upFile = await this.uploadFile(item.full_path, size_limit, part_name_func);
            if (!o_upFile.ok) {
              errors.push(`failed to upload ${path.relative(base_dir, item.full_path)}:${o_upFile.msg}`)
            } else {
              files.push({
                path: path.relative(base_dir, item.full_path),
                chunks: o_upFile.files.map(o => {
                  return {
                    url: o.url,
                    name: o.name
                  }
                })
              });
              break;
            }
          }

        }
      }
      let ok = ($ => {
        if (files.length > 0) return true;
        if (safeList.filter(l => l.stats.isFile() && l.stats.size > 0).length == 0) return true;
        return false;
      })();
      resolve({
        ok: ok,
        errors: errors,
        files: files,
        toHTML: () => {
          let error_html = errors.map(e => `<font color="#BF314C">${e}</font>`).join("<hr>");
          let files_html = files.map(file => {
            if (file.chunks.length == 1) {
              return `<a href="${file.chunks[0].url}?attname=${encodeURIComponent(file.chunks[0].name)}"><font color="#179717"><u>${file.path}</u></font></a>`
            } else {
              return file.chunks.map(chunk => {
                return `<a href="${chunk.url}?attname=${encodeURIComponent(chunk.name)}"><font color="#218a21"><u>${
                  path.join(path.dirname(file.path), chunk.name)
                  }</u></font></a>`
              })
            }
          }).join("<br>");
          return `${error_html ? `<blockquote><h4><font color="#BF341C">ERROR LOGS:</font></h4>${error_html}</blockquote>\n` : ""}` +
            `<blockquote><h4><font color="#5CAD5C">LINKS :</font></h4><p><blockquote>${files_html}</blockquote></p></blockquote>`
        }
      })

    })
  }
}

module.exports = {
  Pianke7Niu: Pianke7Niu
}